package net.moonbiter.ebs.descriptors;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import net.moonbiter.BugException;
import net.moonbiter.OperationFailureException;
import net.moonbiter.ebs.validation.ValidationException;

import org.apache.commons.beanutils.PropertyUtils;

/**
 * Descriptor that create an instance in this way:
 * - first call a constructor that takes no parameters
 * - after set some properties using setter methods.
 * 
 * @author federico tomassetti
 */
public class PropertiesDescriptor<INPUTS, DESCRIBED_TYPE> implements
		Descriptor<INPUTS, DESCRIBED_TYPE> {
	private Constructor<? extends DESCRIBED_TYPE> constructor;
	private Map<String, Method> setters;
	private Map<String, Descriptor<INPUTS, ?>> specificDescriptors;

	/**
	 * Create a descriptor that will use the constructor with no parameters
	 * to instantiate the object. No properties will be setted, if any property
	 * need to be set have to be specified using the addProperty methods.
	 * 
	 * @param instantiatedClass
	 */
	public PropertiesDescriptor(
			Class<? extends DESCRIBED_TYPE> instantiatedClass) {
		if (Modifier.isAbstract(instantiatedClass.getModifiers())){
			throw new IllegalArgumentException("Can't instantiate an abstract class");
		}
		try {
			constructor = instantiatedClass.getConstructor();
		} catch (SecurityException e) {
			throw new IllegalArgumentException(
					"The constructor with no parameters can't be accessed due to security issues",
					e);
		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException(
					"The constructor with no parameters can't be found", e);
		}
		this.setters = new HashMap<String, Method>();
		this.specificDescriptors = new HashMap<String, Descriptor<INPUTS, ?>>();
	}

	/**
	 * The setter must take a parameter that can be "described"
	 * by the specificDescriptor but this can't be checked...
	 * 
	 * @param propertyName
	 * @param setter deve avere un solo parametro
	 * @param specificDescriptor
	 */
	public void addProperty(String propertyName, Method setter,
			Descriptor<INPUTS, ?> specificDescriptor) {
		addProperty(propertyName, setter);
		specificDescriptors.put(propertyName, specificDescriptor);
	}

	public void addProperty(String propertyName, Method setter) {
		if (setter.getParameterTypes().length != 1) {
			throw new IllegalArgumentException();
		}
		setters.put(propertyName, setter);
	}
	
	public Map<String, Descriptor<INPUTS, ?>> getSpecificDescriptors(){
		return this.specificDescriptors;
	}
	
	public Map<String, Method> getSetters(){
		return this.setters;
	}

	public static <STATIC_INPUTS, STATIC_DESCRIBED_TYPE> PropertiesDescriptor<STATIC_INPUTS, STATIC_DESCRIBED_TYPE> createAsBean(
			Class<STATIC_DESCRIBED_TYPE> aClass) {
		PropertiesDescriptor<STATIC_INPUTS, STATIC_DESCRIBED_TYPE> descriptor =
				new PropertiesDescriptor<STATIC_INPUTS, STATIC_DESCRIBED_TYPE>(
				aClass);
		
		java.beans.PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(aClass);
		for (java.beans.PropertyDescriptor aProperty : properties){
			Method setter = aProperty.getWriteMethod();
			if (null!=setter){
				descriptor.addProperty(aProperty.getName(), setter);
			}
		}
		                   
		return descriptor;
	}

	public DESCRIBED_TYPE produce(INPUTS inputs, DescriptorsSet<INPUTS> descriptorsSet)
			throws OperationFailureException, ValidationException {
		DESCRIBED_TYPE instance = null;
		try {
			instance = constructor.newInstance();
		} catch (IllegalArgumentException e) {
			throw new BugException("no paramters are involved");
		} catch (InstantiationException e) {
			throw new BugException("the class was not abstract when we checked in constructors");
		} catch (IllegalAccessException e) {
			throw new OperationFailureException("instantiation",e);
		} catch (InvocationTargetException e) {
			throw new OperationFailureException("instantiation",e);
		}

		for (String propertyName : setters.keySet()) {
			Descriptor<INPUTS,?> descriptor = null;
			if (specificDescriptors.containsKey(propertyName)) {
				descriptor = specificDescriptors.get(propertyName);
			} else {
				Class<?> propertyClass = setters.get(propertyName)
						.getParameterTypes()[0];
				descriptor = descriptorsSet.getDescriptor(propertyClass);
			}

			Object value = descriptor.produce(inputs, descriptorsSet);
			try {
				setters.get(propertyName).invoke(instance,
						new Object[] { value });
			} catch (IllegalArgumentException e) {
				throw new OperationFailureException("setting property \""+propertyName+"\"",e);
			} catch (IllegalAccessException e) {
				throw new OperationFailureException("setting property \""+propertyName+"\"",e);
			} catch (InvocationTargetException e) {
				throw new OperationFailureException("setting property \""+propertyName+"\"",e);
			}
		}
		return instance;
	}

}
